/*! 
 * jquery.ux.slider - v 1.0.0 - http://jquery-ux.com/
 * Copyright (c) 2010 Michael Helgeson, Three Dub Media
 * Open Source MIT License - http://jquery-ux.com/license 
 */
// Created: 2007-02-27
// Updated: 2010-01-22
// REQUIRES: jquery 1.4+, jquery.ux, event.drag

;(function( $ ){ // confine scope

$.ux( "slider", {
	version: '1.0.0',
	expose: "value", // + enable disable destroy
	defaults: {
		value: 50, // the value to be set
		detail: 1, // the numeric value to snap evenly
		min: 0, // the minimum value
		max: 100, // the maximum value
		// interactive behavior
		step: 5, // mousedown/keyclick animation steps
		delay: 40, // mousedown/keyclick animation interval
		 // trigonometric slider layout options...
		angle: 0, // (degrees) 0: left to right, 90: top to bottom  
		length: null, // the actual length of the slider vector
		width: null, // the width used to calculate the vector
		height: null, // the height used to calculate the vector
		path: null, // the +/- range along the track vector
		// optional callback hooks...
		format: null, // optional value formatting (like $cash())
		unformat: null, // optional for cleaning up user entered values
		change: null, // whenever the value changes...
		// generated markup settings
		insert: "before", // string "after" or "before"
		// the actual markup that will create the slider
		markup: function(){
			return ([
				'<div class="', this.classify("wrap"), '">',
					'<div class="', this.classify("track"), '">',
						'<div class="', this.classify("handle-min"), '"></div>',
						'<div class="', this.classify("handle-max"), '"></div>',
						'<div class="', this.classify("handle-range"), '"></div>',
						'</div>',
					'</div>'
				]).join("");
			}
		// classifieds...
		// wrap: "ux-slider-wrap",
		// active: "ux-slider-active",
		// disabled: "ux-slider-disabled",
		// track: "ux-slider-track", 
		// handle: "ux-slider-handle", 
		// input: "ux-slider-input"		
		},
	create: function( elem, opts ){ 
		// store args, extend the instance
		$.extend( this, { input:elem, $input:$( elem ), opts:opts });
		// enforce <input/> node restrictions...
		if ( !this.$input.is(":input") ) return false; 
		// inject the DOM elements...
		this.$handle = $('<a href="#"></a>')
			.addClass( this.classify("handle") );
		this.$track = $("<div></div>")
			.addClass( this.classify("track") )
			.append( this.$handle );
		this.$wrap = $("<div></div>")
			.addClass( this.classify("wrap") )
			.append( this.$track );
		this.$input[ opts.insert ]( this.$wrap )
			.addClass( this.classify("input") );
		// convert degrees to radians...
		opts.angle = Math.PI * opts.angle / 180;	
		// store some trig values (rounded)...
		this.sin = parseFloat( Math.sin( opts.angle ).toFixed( 5 ) );	
		this.abssin = Math.abs( this.sin );
		this.cos = parseFloat( Math.cos( opts.angle ).toFixed( 5 ) );
		this.abscos = Math.abs( this.cos );
		this.tan = parseFloat( Math.tan( opts.angle ).toFixed( 5 ) );
		this.power = -Math.round( Math.log( opts.detail )/Math.LN10 );
		// determine value range and limits...
		this.min = Math.min( opts.min, opts.max );
		this.max = Math.max( opts.min, opts.max );
		opts.length = opts.length || ( this.abssin <= this.abscos ? 
			( opts.width || this.$track.width() )/this.abscos : 
			( opts.height || this.$track.height() )/this.abssin ); 
		this.width = opts.length * this.abscos;
		this.height = opts.length * this.abssin;
		this.pixels = opts.length/( this.max - this.min );
		this.path = opts.path || ( this.abssin <= this.abscos ? 
			this.$handle.height()/this.abssin : 
			this.$handle.width()/this.abscos );
		// bind DOM events...
		this.$input.bind( "blur", $.proxy( this, "handler") );
		this.$handle.bind( "click keydown", $.proxy( this, "handler") );
		this.$wrap.bind( "mousedown dragstart drag dragend mouseup", { 
			drop: false, distance: this.pixels * opts.step - opts.step // "drag" settings
			}, $.proxy( this, "handler") );
		if ( opts.change ) this.$input.bind( "slider:change", opts.change ); // callback
		// set the provided/default value
		this.value( opts.value );
		},
	destroy: function(){
		this.$input.unbind( "blur", $.proxy( this, "value") ).removeClass( this.classify("input") );
		this.$wrap.unbind( "mousedown dragstart drag dragend mouseup", $.proxy( this, "handler") );
		this.$handle.unbind( "click keydown", $.proxy( this, "handler") );
		this.$wrap.remove().empty(); // cleanup the DOM 
		},
	disable: function( disabled ){ 
		this.$input.attr( "disabled", disabled );
		this.$wrap.toggleClass( this.classify("disabled"), disabled );
		},
	methods: {
		handler: function( event ){
			var self = this, val = this.val;
			clearTimeout( this.timer );
			if ( !this.disabled ) switch ( event.type ){
				case 'blur': // <input />
					this.value( this.opts.unformat ? 
						this.opts.unformat.call( this.input, this.input.value ) : 
						this.input.value );
					break;
				case 'mousedown': // <div.wrap />
					this.offset = this.$track.offset();	
					if ( this.offThePath( event ) ) break;
					val = this.event2value( event );
					this.timer = setTimeout(function(){
						if ( self.dragging || self.val === val ) return; // stop
						self.value( val > self.val ? 
							Math.min( val, self.val + self.opts.step ) : 
							Math.max( val, self.val - self.opts.step )
							); 
						self.timer = setTimeout( arguments.callee, self.opts.delay ); // recurse...
						}, this.opts.delay );
					break;
				case !this.dragging && 'dragstart':  // <div.wrap />
					if ( this.offThePath( event ) ) return false;
					this.$wrap.addClass( this.classify("active") );	
					this.dragging = true;				
					break;
				case this.dragging && 'drag':  // <div.wrap />
					this.value( this.event2value( event ) ); 
					break;
				case 'dragend': // <div.wrap />
					this.$wrap.removeClass( this.classify("active") );
					this.dragging = false;
					break;
				case 'keydown': // <a.handle />
					switch ( event.keyCode ){
						case 35: // END
							this.value( this.max );
							break;
						case 36: // HOME
							this.value( this.min );
							break;						
						case this.cos > 0 && 37: // LEFT
						case this.sin > 0 && 38: // UP	
						case this.cos < 0 && 39: // RIGHT
						case this.sin < 0 && 40: // DOWN
							this.value( val - this.opts.step );
							break;
						case this.cos < 0 && 37: // LEFT	
						case this.sin < 0 && 38: // UP	
						case this.cos > 0 && 39: // RIGHT
						case this.sin > 0 && 40: // DOWN
							this.value( val + this.opts.step );
							break;							
						default: return;
						}
				case 'click': // <a.handle />
					event.target.focus();
					return false;
				}
			},	
		event2value: function( ev ){
			var x = ev.pageX - this.offset.left, y = ev.pageY - this.offset.top;
			x = ( this.cos > 0 ? x : x - this.width )/this.cos;
			y = ( this.sin > 0 ? y : y - this.height )/this.sin;
			return this.range( this.min + Math.max( 
				( isNaN(x) || !isFinite(x) ) ? 0 : x,
				( isNaN(y) || !isFinite(y) ) ? 0 : y
				) / this.pixels );
			},
		offThePath: function( ev ){
			var x = ev.pageX - this.offset.left, y = ev.pageY - this.offset.top, dist;
			x = this.cos > 0 ? x : x - this.width;
			y = this.sin > 0 ? y : y - this.height;
			dist = this.cos ? this.cos*( x * this.tan - y ) : this.sin*( y / this.tan - x );
			return Math.abs( dist ) > this.path;
			},
		value: function( val ){
			if ( val === undefined ) 
				return this.input.value;
			val = this.range( val );	
			this.publish( "slide", val ); // "slider:slide"
			if ( !isNaN( val ) && val != this.val ){ // value changed
				this.val = val; // remember the value
				this.$input.val( this.opts.format ? 
					this.opts.format.call( this.input, val ) : 
					val );				
				val = ( val - this.min ) * this.pixels; // convert to distance
				this.$handle.css({ // calculate position
					left: ( this.cos > 0 ? 0 : this.width )+( this.cos * val ), 
					top: ( this.sin > 0 ? 0 : this.height )+( this.sin * val ) 
					}); 
				this.publish( "change", val ); // "slider:change"
				}
			},
		range: function( val ){
			val = Math.round( val / this.opts.detail ) * this.opts.detail; 
			val = Math.max( Math.min( this.max, val ), this.min );
		 	return parseFloat( isFinite( this.power ) ? val.toFixed( this.power ) : val );
			}
		}
	});

})( jQuery ); // secure the $ jQuery alias